package cxm.proj.xtalk.smack;

import java.io.File;
import java.io.RandomAccessFile;

import cxm.lib.alg.RingBuffer;
import cxm.lib.sys.AsyncStartMachine;
import cxm.lib.util.MyLog;
import cxm.lib.util.StorageHelper;

public class SoundMixer extends AsyncStartMachine {
	private int trackCount = 0;
	private int dataLen = 0;
	private int totalWriteLength = 0;
	private File saveFile = null;
	private RandomAccessFile fileStream = null;
	// private LinkedList<LinkedList<byte[]>> dataList = null;
	private RingBuffer []ringBuffer = null;
	private byte []mixBuffer = null;
	private byte []data0Buffer = null;
	private byte []data1Buffer = null;

	public SoundMixer(int trackCount, int dataLen, String saveFile) {
		if (trackCount != 2 || null == saveFile)
			throw new IllegalStateException("Not support");
		this.trackCount = trackCount;
		this.dataLen = dataLen;
		this.saveFile = StorageHelper.getFile(
				StorageHelper.StorageType.StorageExtrenal, saveFile + ".wav");
		this.mixBuffer = new byte[this.dataLen];
		this.data0Buffer = new byte[this.dataLen];
		this.data1Buffer = new byte[this.dataLen];

		// construct list
		this.ringBuffer = new RingBuffer[2];
		this.ringBuffer[0] = new RingBuffer();
		this.ringBuffer[1] = new RingBuffer();
	}
	
	public String getSaveFile() {
		return this.saveFile.getAbsolutePath();
	}
	
	public void putData(int trackIndex, byte []data) {
		if (trackIndex < 0 || trackIndex >= trackCount ||
				data.length != this.dataLen)
			throw new IllegalArgumentException("trackIndex | data");

		this.ringBuffer[trackIndex].putBuffer(data);
		if (dataLen != data.length)
			MyLog.Log(MyLog.Level.Error, "Data lost with: " +
					(data.length - dataLen));
	}
	
	public void flush() {
		// TODO waiting for data processed
		// remain empty
	}

	@Override
	protected boolean onBeforeRun() {
		// Sleep a while for connection established
		try {
			Thread.sleep(1000, 0);
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot sleep for a while: " + e);
		}
		
		// GC
		System.gc();
		
		// lower level thread
		android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
		try {
			this.fileStream = new RandomAccessFile(this.saveFile, "rw");
			this.fileStream.seek(0);

			return true;
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot open file to write: " + e);
			return false;
		}
	}

	@Override
	protected void onRunOnce() {
		// first check data allow
		if (!this.ringBuffer[0].hasData(this.data0Buffer.length) ||
				!this.ringBuffer[1].hasData(this.data0Buffer.length))
			return;

		// mixed
		int data0Len = this.ringBuffer[0].getBuffer(data0Buffer);
		int data1Len = this.ringBuffer[1].getBuffer(data1Buffer);
		if (data0Len != data1Len)
			MyLog.Log(MyLog.Level.Warning, String.format("Data missed because mismatch %d : %d",
					data0Len, data1Len));
		int saveCount = data0Len >= data1Len ? data1Len : data0Len; // use less buffer
		for (int i = 0; i < saveCount; i++) {
			float samplef0 = data0Buffer[i] / 128.0f;
			float samplef1 = data1Buffer[i] / 128.0f;

			float mixed = samplef0 + samplef1;
			mixed *= 0.8;
			if (mixed > 1.0f)
				mixed = 1.0f;
			if (mixed < -1.0f)
				mixed = -1.0f;
			this.mixBuffer[i] = (byte)(mixed * 128.0f);
		}

		// save data
		this.onSaveData(this.mixBuffer, dataLen);
	}

	@Override
	protected void onAfterRun() {
		// close file
		try {
			// seek to start, write wave head
			this.fileStream.seek(0);

            this.fileStream.write("RIFF".getBytes());
            // little endian
            int chunkSize = 36 + this.totalWriteLength;
            this.fileStream.write(new byte[] { (byte) (chunkSize), (byte) (chunkSize >> 8),
                    (byte) (chunkSize >> 16), (byte) (chunkSize >> 24) });
            this.fileStream.write("WAVE".getBytes());
            this.fileStream.write("fmt ".getBytes());
            int subChunk1Size = 16;
            this.fileStream.write(new byte[] { (byte) (subChunk1Size),
                    (byte) (subChunk1Size >> 8), (byte) (subChunk1Size >> 16),
                    (byte) (subChunk1Size >> 24) });
            int audioFormat = 1;
            this.fileStream.write(new byte[] { (byte) (audioFormat),
                    (byte) (audioFormat >> 8) });
            int channels = 1;
            this.fileStream.write(new byte[] { (byte) (channels), (byte) (channels >> 8) });
            int sampleRate = 8000;
            this.fileStream.write(new byte[] { (byte) (sampleRate),
                    (byte) (sampleRate >> 8), (byte) (sampleRate >> 16),
                    (byte) (sampleRate >> 24) });
            int byteRate = 16000;
            this.fileStream.write(new byte[] { (byte) (byteRate), (byte) (byteRate >> 8),
                    (byte) (byteRate >> 16), (byte) (byteRate >> 24) });
            int blockAlign = 2;
            this.fileStream.write(new byte[] { (byte) (blockAlign),
                    (byte) (blockAlign >> 8) });
            int bitsPerSample = 16;
            this.fileStream.write(new byte[] { (byte) (bitsPerSample),
                    (byte) (bitsPerSample >> 8) });
            this.fileStream.write("data".getBytes());
            int subChunk2Size = this.totalWriteLength;
            this.fileStream.write(new byte[] { (byte) (subChunk2Size),
                    (byte) (subChunk2Size >> 8), (byte) (subChunk2Size >> 16),
                    (byte) (subChunk2Size >> 24) });

			this.fileStream.close();
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot flush file: " + e);
		}
	}
	
	protected void onSaveData(byte []data, int len) {
		try {
			this.fileStream.write(data, 0, len);
			this.totalWriteLength += len;
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot write bytes: " + data);
		}
	}
}
